﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using LightEffects;
using System.Threading.Tasks;
using Sirian.DrawableObject;

namespace DrawableObjects
{
    /// <summary>
    /// This class represents the terrain of the game.
    /// </summary>
    public class Terrain : DrawableObject
    {
        private const float ZONE_MERGE_AREA_FACTOR = 0.3f;

        private GraphicsDevice graphicsDevice
        {
            get;
            set;
        }

        public int progress
        {
            get;
            set;
        }

        private List<VertexTerrain[]> vertices_list;

        private int[] indices;

        private int[] doubleIndices;

        private List<float[,]> heightData_list;

        private int terrainWidth
        {
            get;
            set;
        }

        private int terrainHeight
        {
            get;
            set;
        }

        public int terrainHeight_part
        {
            get;
            private set;
        }

        private const int COUNT_TERRAIN_ZONES = 3;

        private const int COUNT_HEIGHT_AREAS = 4;

        private float terrainMinHeight
        {
            get;
            set;
        }

        public float terrainMaxHeight
        {
            get;
            private set;
        }

        private Texture2D[] heightMap
        {
            get;
            set;
        }

        private Texture2D[] terrainTextures
        {
            get;
            set;
        }

        private DynamicVertexBuffer deviceVertexBuffer
        {
            get;
            set;
        }

        private IndexBuffer deviceIndexBuffer
        {
            get;
            set;
        }

        public int currentTerrainPart
        {
            get;
            private set;
        }

        public int nextTerrainPart
        {
            get;
            private set;
        }

        private VertexTerrain[] mergedVertices;


        /// <summary>
        /// Constructor of this class. It calculates all relevant data information of the terrain.
        /// </summary>
	    public Terrain(Texture2D[] heightMap, Texture2D[] terrainTextures, GraphicsDevice device) : base(new Matrix())
	    {
            this.heightMap = heightMap;
            this.terrainTextures = terrainTextures;
            this.graphicsDevice = device;
            terrainHeight_part = heightMap[0].Height;

            heightData_list = new List<float[,]>();
            vertices_list = new List<VertexTerrain[]>();

            progress = 0;

            CreateHeightData(heightMap);
            CreateVertices();

            indices = CreateIndices(terrainWidth, terrainHeight_part);

            doubleIndices = CreateIndices(terrainWidth, terrainHeight_part * 2);
            deviceIndexBuffer = new IndexBuffer(device, typeof(int), doubleIndices.Length, BufferUsage.WriteOnly);
            deviceIndexBuffer.SetData(doubleIndices);

            CalculateNormals();

            Update();
        }

        /// <summary>
        /// Renders the current and next part of the terrain.
        /// </summary>
        public void Draw(Effect shader)
        {
            graphicsDevice.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0);
            graphicsDevice.BlendState = BlendState.Opaque;
            for (int i = 0; i < 15; i++)
            {
                graphicsDevice.SamplerStates[i] = SamplerState.PointWrap;
            }
            shader.Parameters["xTexture0"].SetValue(terrainTextures[7]);
            shader.Parameters["xTexture1"].SetValue(terrainTextures[6]);
            shader.Parameters["xTexture2"].SetValue(terrainTextures[5]);
            shader.Parameters["xTexture3"].SetValue(terrainTextures[4]);

            shader.Parameters["xTexture4"].SetValue(terrainTextures[11]);
            shader.Parameters["xTexture5"].SetValue(terrainTextures[10]);
            shader.Parameters["xTexture6"].SetValue(terrainTextures[9]);
            shader.Parameters["xTexture7"].SetValue(terrainTextures[8]);

            shader.Parameters["xTexture8"].SetValue(terrainTextures[3]);
            shader.Parameters["xTexture9"].SetValue(terrainTextures[2]);
            shader.Parameters["xTexture10"].SetValue(terrainTextures[1]);
            shader.Parameters["xTexture11"].SetValue(terrainTextures[0]);

            //Draw the current part of the terrain and move it in front of the camera.
            shader.Parameters["xWorld"].SetValue(Matrix.Identity * Matrix.CreateTranslation(-76.0f, 0, 0));
            shader.CurrentTechnique.Passes[0].Apply();

            //Set vertex and index buffers of the graphics device
            graphicsDevice.Indices = deviceIndexBuffer;
            graphicsDevice.SetVertexBuffer(deviceVertexBuffer);

            graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, mergedVertices.Length, 0, doubleIndices.Length / 3);
        }

        /// <summary>
        /// Indicates which parts of the terrain should be copied into the graphicsdevice buffers.
        /// </summary>
        public override void Update()
        {     
            currentTerrainPart = progress % heightMap.Length;
            nextTerrainPart = (progress + 1) % heightMap.Length;
            CopyToDeviceBuffers();
        }
 
        /// <summary>
        /// Merges the vertices of two terrain parts into one array.
        /// </summary>
        /// <param name="vt1">The given vertices of the one part of the terrain.</param>
        /// <param name="vt2">The given vertices of the other part of the terrain.</param>
        /// <returns>The merged vertices of the two parts of the terrain.</returns>
        private VertexTerrain[] MergeVertices(VertexTerrain[] vt1, VertexTerrain[] vt2) {
            VertexTerrain[] mergedVertices = new VertexTerrain[vt1.Length + vt2.Length];
            for (int i = 0; i < vt1.Length; i++) {
                mergedVertices[i] = vt1[i];
            }
            for (int i = vt1.Length; i < vt2.Length + vt1.Length; i++) {
                mergedVertices[i] = vt2[i - vt1.Length];
            }

            return mergedVertices;
        }


        /// <summary>
        /// Reads out the height data from the given heightmap textures. The height data is the red value / 5 of each pixel.
        /// Creates foreach heightmap texture a heightData array for the calculated values and add it to the list heightData_list.
        /// Reads out the maximum and minimum height of the terrain.
        /// </summary>
        /// <param name="terrainWidth">The given heightmap textures.</param>
        private void CreateHeightData(Texture2D[] heightMap)
        {
            terrainWidth = heightMap[0].Width;
            terrainHeight = terrainHeight_part * heightMap.Length;


            List<Color[]> tempColorData = new List<Color[]>();
            for (int i = 0; i < heightMap.Length; i++)
            {
                Color[] temp = new Color[terrainWidth * terrainHeight_part];
                heightMap[i].GetData(temp);
                tempColorData.Add(temp);
            }

            //Create the height array and extracts the minimum and maximum height of the terrain
            terrainMaxHeight = 0;
            terrainMinHeight = Int32.MaxValue;

            foreach (Color[] colorDataObject in tempColorData)
            {
                float[,] heightData = new float[terrainWidth, terrainHeight_part];
                for (int x = 0; x < terrainWidth; x++)
                {
                    for (int y = 0; y < terrainHeight_part; y++)
                    {
                        heightData[x, y] = colorDataObject[x + (y * terrainWidth)].R / 5.0f;

                        if (heightData[x, y] < terrainMinHeight)
                            terrainMinHeight = heightData[x, y];
                        if (heightData[x, y] > terrainMaxHeight)
                            terrainMaxHeight = heightData[x, y];
                    }
                }
                heightData_list.Add(heightData);
            }
        }

        /// <summary>
        /// Creates the vertices of the primitives which are used to draw the terrain.
        /// </summary>
        private void CreateVertices()
        {
            //Creates the height areas
            float areaHeight;
            float zoneLength;
            areaHeight = (terrainMaxHeight - terrainMinHeight) / (COUNT_HEIGHT_AREAS - 1);

            //Creates the terrain zones
            float zoneMergeArea = (terrainHeight / COUNT_TERRAIN_ZONES) * ZONE_MERGE_AREA_FACTOR;
            zoneLength = (terrainHeight - (COUNT_TERRAIN_ZONES - 1) * zoneMergeArea) / COUNT_TERRAIN_ZONES;

            //Creates the vertices of the terrain
            float[,] heightData = GetAllHeightData(heightData_list);
            int terrainPart = 0;

            for (int p = 0; p < heightMap.Length; p++)
            {
                VertexTerrain[] temp_vertices = new VertexTerrain[terrainWidth * terrainHeight_part];
                Parallel.For(0, terrainWidth, x =>  
                {
                    for (int y = 0; y < terrainHeight_part; y++)
                    {
                        temp_vertices[x + (y * terrainWidth)].position = new Vector3(x, heightData[x, y + (terrainPart * terrainHeight_part)], -(y + (p * terrainHeight_part)));

                        temp_vertices[x + y * terrainWidth].textureCoordinate.X = (float)x / 30.0f;
                        temp_vertices[x + y * terrainWidth].textureCoordinate.Y = (float)y / 30.0f;

                        float zoneMoonfactor = 0;
                        float zoneMarsFactor = 0;
                        float zoneLavaFactor = 0;

                        zoneMoonfactor = MathHelper.Clamp(1 - ((y + (terrainPart * terrainHeight_part)) - zoneLength) / zoneMergeArea, 0, 1);
                        zoneLavaFactor = MathHelper.Clamp(((y + (terrainPart * terrainHeight_part)) - (2 * zoneLength + zoneMergeArea)) / zoneMergeArea, 0, 1);
                        zoneMarsFactor = MathHelper.Clamp((((y + (terrainPart * terrainHeight_part)) - zoneLength) / zoneMergeArea), 0, 1) - zoneLavaFactor;

                        temp_vertices[x + y * terrainWidth].texWeightsMoon.X = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y + (terrainPart * terrainHeight_part)] - terrainMinHeight) / (areaHeight), 0, 1) * zoneMoonfactor;
                        temp_vertices[x + y * terrainWidth].texWeightsMoon.Y = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y + (terrainPart * terrainHeight_part)] - areaHeight) / (areaHeight), 0, 1) * zoneMoonfactor;
                        temp_vertices[x + y * terrainWidth].texWeightsMoon.Z = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y + (terrainPart * terrainHeight_part)] - (2 * areaHeight)) / (areaHeight), 0, 1) * zoneMoonfactor;
                        temp_vertices[x + y * terrainWidth].texWeightsMoon.W = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y + (terrainPart * terrainHeight_part)] - terrainMaxHeight) / (areaHeight), 0, 1) * zoneMoonfactor;

                        temp_vertices[x + y * terrainWidth].texWeightsMars.X = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y + (terrainPart * terrainHeight_part)] - terrainMinHeight) / (areaHeight), 0, 1) * zoneMarsFactor;
                        temp_vertices[x + y * terrainWidth].texWeightsMars.Y = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y + (terrainPart * terrainHeight_part)] - areaHeight) / (areaHeight), 0, 1) * zoneMarsFactor;
                        temp_vertices[x + y * terrainWidth].texWeightsMars.Z = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y + (terrainPart * terrainHeight_part)] - (2 * areaHeight)) / (areaHeight), 0, 1) * zoneMarsFactor;
                        temp_vertices[x + y * terrainWidth].texWeightsMars.W = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y + (terrainPart * terrainHeight_part)] - terrainMaxHeight) / (areaHeight), 0, 1) * zoneMarsFactor;

                        temp_vertices[x + y * terrainWidth].texWeightsLava.X = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y + (terrainPart * terrainHeight_part)] - terrainMinHeight) / (areaHeight), 0, 1) * zoneLavaFactor;
                        temp_vertices[x + y * terrainWidth].texWeightsLava.Y = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y + (terrainPart * terrainHeight_part)] - areaHeight) / (areaHeight), 0, 1) * zoneLavaFactor;
                        temp_vertices[x + y * terrainWidth].texWeightsLava.Z = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y + (terrainPart * terrainHeight_part)] - (2 * areaHeight)) / (areaHeight), 0, 1) * zoneLavaFactor;
                        temp_vertices[x + y * terrainWidth].texWeightsLava.W = MathHelper.Clamp(1.0f - Math.Abs(heightData[x, y + (terrainPart * terrainHeight_part)] - terrainMaxHeight) / (areaHeight), 0, 1) * zoneLavaFactor;


                        float total = temp_vertices[x + y * terrainWidth].texWeightsMoon.X;
                        total += temp_vertices[x + y * terrainWidth].texWeightsMoon.Y;
                        total += temp_vertices[x + y * terrainWidth].texWeightsMoon.Z;
                        total += temp_vertices[x + y * terrainWidth].texWeightsMoon.W;

                        total += temp_vertices[x + y * terrainWidth].texWeightsMars.X;
                        total += temp_vertices[x + y * terrainWidth].texWeightsMars.Y;
                        total += temp_vertices[x + y * terrainWidth].texWeightsMars.Z;
                        total += temp_vertices[x + y * terrainWidth].texWeightsMars.W;

                        total += temp_vertices[x + y * terrainWidth].texWeightsLava.X;
                        total += temp_vertices[x + y * terrainWidth].texWeightsLava.Y;
                        total += temp_vertices[x + y * terrainWidth].texWeightsLava.Z;
                        total += temp_vertices[x + y * terrainWidth].texWeightsLava.W;

                        temp_vertices[x + y * terrainWidth].texWeightsMoon.X /= total;
                        temp_vertices[x + y * terrainWidth].texWeightsMoon.Y /= total;
                        temp_vertices[x + y * terrainWidth].texWeightsMoon.Z /= total;
                        temp_vertices[x + y * terrainWidth].texWeightsMoon.W /= total;

                        temp_vertices[x + y * terrainWidth].texWeightsMars.X /= total;
                        temp_vertices[x + y * terrainWidth].texWeightsMars.Y /= total;
                        temp_vertices[x + y * terrainWidth].texWeightsMars.Z /= total;
                        temp_vertices[x + y * terrainWidth].texWeightsMars.W /= total;

                        temp_vertices[x + y * terrainWidth].texWeightsLava.X /= total;
                        temp_vertices[x + y * terrainWidth].texWeightsLava.Y /= total;
                        temp_vertices[x + y * terrainWidth].texWeightsLava.Z /= total;
                        temp_vertices[x + y * terrainWidth].texWeightsLava.W /= total;

                    }   
                });
                vertices_list.Add(temp_vertices);
                terrainPart++;
            }
        }

        /// <summary>
        /// Creates the indices, in which order to draw the vertices.
        /// </summary>
        /// <param name="terrainWidth">The given width of the terrain.</param>
        /// <param name="terrainHeight_part">The given height of one part of the terrain.</param>
        /// <returns>One array of the created indices.</returns>
        private int[] CreateIndices(int terrainWidth, int terrainHeight_part)
        {
            int[] indices = new int[(terrainWidth - 1) * (terrainHeight_part - 1) * 6];
            int counter = 0;
            for (int y = 0; y < terrainHeight_part - 1; y++)
            {
                for (int x = 0; x < terrainWidth - 1; x++)
                {
                    int lowerLeft = x + y * terrainWidth;
                    int lowerRight = (x + 1) + y * terrainWidth;
                    int topLeft = x + (y + 1) * terrainWidth;
                    int topRight = (x + 1) + (y + 1) * terrainWidth;

                    indices[counter++] = topLeft;
                    indices[counter++] = lowerRight;
                    indices[counter++] = lowerLeft;

                    indices[counter++] = topLeft;
                    indices[counter++] = topRight;
                    indices[counter++] = lowerRight;

                }
            }
            return indices;
        }

        /// <summary>
        /// Calculates the normals that are used for lighting.
        /// </summary>
        private void CalculateNormals()
        {
            int j = 0;
            foreach (VertexTerrain[] verticesObject in vertices_list)   
            {
                Parallel.For(0, verticesObject.Length, i =>   
                    verticesObject[i].normal = new Vector3(0, 0, 0));

                Parallel.For(0, indices.Length / 3, i =>  
                {
                    int index1 = indices[i * 3];
                    int index2 = indices[i * 3 + 1];
                    int index3 = indices[i * 3 + 2];

                    Vector3 side1 = verticesObject[index1].position - verticesObject[index3].position;
                    Vector3 side2 = verticesObject[index1].position - verticesObject[index2].position;
                    Vector3 normal = Vector3.Cross(side1, side2);

                    verticesObject[index1].normal += normal;
                    verticesObject[index2].normal += normal;
                    verticesObject[index3].normal += normal;
                });

                Parallel.For(0, verticesObject.Length, i =>
                    verticesObject[i].normal.Normalize());

                j++;
            }
        }

        /// <summary>
        /// This is called when a new terrain part was created. It loads the vertices of the current
        /// and next part of the terrain into the vertexbuffer of the graphicsdevice.
        /// </summary>
        public void CopyToDeviceBuffers()
        {
            //the current terrain part vertices
            if (deviceVertexBuffer != null)
            {
                deviceVertexBuffer.Dispose();
            }

            mergedVertices = MergeVertices(vertices_list[currentTerrainPart], vertices_list[nextTerrainPart]);

            deviceVertexBuffer = new DynamicVertexBuffer(graphicsDevice, VertexTerrain.vertexDeclaration, mergedVertices.Length, BufferUsage.WriteOnly);
            deviceVertexBuffer.SetData(mergedVertices);
        }

        /// <summary>
        /// Creates one array of height data of all elements from the given list of height data arrays.
        /// </summary>
        /// <param name="heightData_List">The given list of height data arrays.</param>
        /// <returns>One array of all heightdata from the given list.</returns>
        private float[,] GetAllHeightData(List<float[,]> heightData_List)
        {
            float[,] heightData = new float[terrainWidth, terrainHeight];
            int terrainPart = 0;
            foreach (float[,] heightDataObject in heightData_list)
            {
                Parallel.For(0, terrainWidth, x =>
                {
                    for (int y = 0; y < terrainHeight_part; y++)
                    {
                        heightData[x, y + (terrainPart * terrainHeight_part)] = heightDataObject[x, y];
                    }
                });
                terrainPart++;
            }
            return heightData; ;
        }

        /// <summary>
        /// Sets the progress value of the terrain to zero to indicate which terrain part should be drawn.
        /// </summary>
        public void RestartTerrain()
        {
            progress = 0;
            currentTerrainPart = 0;
            nextTerrainPart = 1;
            Update();
        }

    }
}

